﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Reflection;

namespace Td.Additional.Log
{
    /// <summary>
    /// Base file log class, writes to text file by default.
    /// </summary>
    public class FileLog : Log, IDisposable
    {
        #region Properties

        /// <summary>
        /// Gets or sets the log file directory.
        /// </summary>
        /// <value>The log file directory.</value>
        public String LogFileDirectory { get; set; }

        /// <summary>
        /// Gets or sets the file size limit.
        /// </summary>
        /// <value>The file size limit.</value>
        public long FileSizeLimit { get; set; }

        /// <summary>
        /// Gets or sets the file count limit.
        /// </summary>
        /// <value>The file count limit.</value>
        public int FileCountLimit { get; set; }

        /// <summary>
        /// Gets or sets the overall size limit.
        /// </summary>
        /// <value>The overall size limit.</value>
        public long OverallSizeLimit { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether [serialy numbered files].
        /// </summary>
        /// <value>
        /// 	<c>true</c> if [serialy numbered files]; otherwise, <c>false</c>.
        /// </value>
        public Boolean SerialyNumberedFiles { get; set; }

        /// <summary>
        /// Gets or sets the file name format.
        /// </summary>
        /// <value>The file name format.</value>
        public String FileNameFormat { get; set; }

        /// <summary>
        /// Gets or sets the encoder.
        /// </summary>
        /// <value>The encoder.</value>
        public Encoding Encoder { get; set; }

        #endregion

        #region Private fields

        private FileStream fileStream;

        private String currentFilename;

        private DateTime fileCreationDate;

        #endregion

        #region Lifetime management

        /// <summary>
        /// Initializes a new instance of the <see cref="FileLog"/> class.
        /// </summary>
        public FileLog()
            : base()
        {
            LogLevel = LogLevel.Info;
            Encoder = System.Text.Encoding.UTF8;
            LogFileDirectory = AppDomain.CurrentDomain.BaseDirectory;
            Assembly assembly = Assembly.GetEntryAssembly();
            FileNameFormat = Path.GetFileNameWithoutExtension(assembly.Location) + "-{0:yyyy-MM-dd}.log";
            fileCreationDate = DateTime.MinValue;
            FileSizeLimit = 1024 * 1024 * 10; // 10 mb
            FileCountLimit = 100;
            SerialyNumberedFiles = true;
            OverallSizeLimit = 1024 * 1024 * 1024;// 1 GB
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="FileLog"/> is reclaimed by garbage collection.
        /// </summary>
        ~FileLog()
        {
            Dispose(false);
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// True, if resources are already disposed, otherwise false
        /// </summary>
        private bool disposed;
        /// <summary>
        /// Gets a flag indicating if the object has been already disposed.
        /// </summary>
        protected bool Disposed
        {
            get { return disposed; }
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    CloseFileStream();
                }
                disposed = true;
            }
        }

        #endregion

        #region FileStream control

        private void OpenFileStream()
        {
            if (fileStream == null)
            {
                try
                {
                    currentFilename =
                        Path.Combine(LogFileDirectory, String.Format(FileNameFormat, DateTime.Now));
                    fileStream = new FileStream(
                        currentFilename,
                        FileMode.Append,
                        FileAccess.Write,
                        FileShare.Read);
                    CheckLimits();
                    FileInfo fileInfo = new FileInfo(currentFilename);
                    fileCreationDate = fileInfo.CreationTime;
                    OnLogFileOpened(this, new LogFileOpenedEventArgs(this));
                }
                catch (Exception ex)
                {
                    currentFilename = String.Empty;
                    Td.Additional.Debug.ConsoleReport("Error opening log file: " + ex.Message);
                    throw;
                }
            }
        }

        private void CloseFileStream()
        {
            if (fileStream != null)
            {
                OnClosingLogFile(this, new ClosingLogFileEventArgs(this));
                lock (fileStream)
                    try
                    {
                        fileStream.Flush();
                        fileStream.Close();
                    }
                    catch (Exception ex)
                    {
                        Td.Additional.Debug.ConsoleReport("Error closing log file: " + ex.Message);
                    }
                fileStream = null;
            }
        }

        private String createBackupFileName(String fileName, int backupCounter)
        {
            int pointIndex = fileName.LastIndexOf(".");
            if (pointIndex > 0)
                fileName = fileName.Insert(pointIndex, "_" + backupCounter.ToString());
            else
                fileName += "_" + backupCounter.ToString();
            return fileName;
        }

        private void CheckLimits()
        {
            Boolean fileSizeLimitReached =
                (FileSizeLimit > 0 && fileStream != null) ? fileStream.Length > FileSizeLimit : false;
            if (FileCountLimit > 0 || OverallSizeLimit > 0 || fileSizeLimitReached)
            {
                CloseFileStream();
                // Search existing backup files
                long completeFileSize = 0;
                String filePattern = String.Format(FileNameFormat, "*");
                List<FileInfo> logFiles = new List<FileInfo>();
                logFiles.AddRange(new DirectoryInfo(LogFileDirectory).GetFiles(
                    filePattern));
                foreach (var fileInfo in logFiles)
                    completeFileSize += fileInfo.Length;

                // Delete overdued files
                int infiniteLoopCounter = logFiles.Count;
                while ((FileCountLimit > 0 && logFiles.Count >= FileCountLimit) || (OverallSizeLimit > 0 && completeFileSize > OverallSizeLimit))
                {
                    if (infiniteLoopCounter-- < 0)
                        break;
                    DateTime oldestFileDate = DateTime.Now;
                    FileInfo oldestFile = null;
                    foreach (var fileInfo in logFiles)
                        if (fileInfo.LastWriteTime < oldestFileDate)
                        {
                            oldestFileDate = fileInfo.LastWriteTime;
                            oldestFile = fileInfo;
                        }
                    if (oldestFile != null)
                    {
                        var e = new DeletingLogFileEventArgs(oldestFile.FullName);
                        OnDeletingLogFile(this, e);
                        if (!e.Cancel)
                            try
                            {
                                File.Delete(oldestFile.FullName);
                            }
                            catch (Exception ex)
                            {
                                Td.Additional.Debug.ConsoleReport("Error deleting old log file: " + ex.Message);
                            }
                        logFiles.Remove(oldestFile);
                        completeFileSize -= oldestFile.Length;
                    }
                    else
                        break;
                }
                int backupFileCounter = 0;
                if (SerialyNumberedFiles)
                {
                    var lastDate = DateTime.MinValue;
                    foreach (var fileInfo in logFiles)
                    {
                        if (lastDate < fileInfo.LastWriteTime)
                        {
                            lastDate = fileInfo.LastWriteTime;
                            backupFileCounter = 0;
                        }
                        var number = 0;
                        var index = fileInfo.Name.LastIndexOf('_');
                        var point = fileInfo.Name.LastIndexOf('.');
                        if (point < 0) 
                            point = fileInfo.Name.Length - 1;
                        if (index > 0)
                            number = int.Parse(fileInfo.Name.Substring(index + 1, point - index - 1));
                        if (number > backupFileCounter)
                            backupFileCounter = number;
                    }
                    backupFileCounter++;
                }
                if (fileSizeLimitReached)
                {
                    try
                    {
                        if (!SerialyNumberedFiles)
                        {
                            backupFileCounter = 1;
                            while (File.Exists(createBackupFileName(currentFilename, backupFileCounter)))
                                backupFileCounter++;
                        }
                        File.Copy(currentFilename, createBackupFileName(currentFilename, backupFileCounter), true);
                        File.Delete(currentFilename);//, createBackupFileName(currentFilename, backupFileCounter));
                    }
                    catch (Exception ex)
                    {
                        Td.Additional.Debug.ConsoleReport("Error backup current log file: " + ex.Message);
                    }
                }
                fileStream = new FileStream(
                    currentFilename,
                    FileMode.Append,
                    FileAccess.Write,
                    FileShare.Read);
            }
        }

        #endregion

        #region Virtual methods

        /// <summary>
        /// Creates the log entry.
        /// </summary>
        /// <param name="time">The time.</param>
        /// <param name="level">The level.</param>
        /// <param name="method">The method.</param>
        /// <param name="message">The message.</param>
        /// <returns></returns>
        public virtual String CreateLogEntry(DateTime time, LogLevel level, String method, String message)
        {
            return String.Format("{0}\t{1}\t{2}\t{3}{4}", 
                DateTime.Now, 
                level, 
                method, 
                message, 
                System.Environment.NewLine);
        }

        #endregion

        #region Overriden Log methods

        /// <summary>
        /// Writes the log entry.
        /// </summary>
        /// <param name="level">The level.</param>
        /// <param name="source">The source.</param>
        /// <param name="message">The message.</param>
        public override void WriteLogEntry(LogLevel level, string source, string message)
        {
            if (disposed)
                return;
            try
            {
                if (!DateTime.Today.Equals(fileCreationDate.Date))
                    CloseFileStream();
                if (fileStream == null)
                    OpenFileStream();
                if (fileStream != null)
                {
                    lock (fileStream)
                    {
                        byte[] lineBytes = Encoder.GetBytes(CreateLogEntry(DateTime.Now, level, source, message));
                        fileStream.Write(lineBytes, 0, lineBytes.Length);
                        fileStream.Flush();
                    }
                    if (fileStream.Length > FileSizeLimit)
                        CloseFileStream();
                }
            }
            catch (Exception ex)
            {
                Td.Additional.Debug.ConsoleReport("Error writing to log file: " + ex.Message);
            }
        }

        #endregion

        #region FileStream event handler

        /// <summary>
        /// Occurs when deleting log file.
        /// </summary>
        public event EventHandler<DeletingLogFileEventArgs> DeletingLogFile;

        /// <summary>
        /// Called when deleting log file.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="Td.Additional.Log.DeletingLogFileEventArgs"/> instance containing the event data.</param>
        protected virtual void OnDeletingLogFile(Object sender, DeletingLogFileEventArgs e)
        {
            EventHandler<DeletingLogFileEventArgs> tempDeletingLogFile = DeletingLogFile;
            if (tempDeletingLogFile != null)
                tempDeletingLogFile(sender, e);
        }

        /// <summary>
        /// Occurs when log file opened.
        /// </summary>
        public event EventHandler<LogFileOpenedEventArgs> LogFileOpened;

        /// <summary>
        /// Called when log file opened.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="Td.Additional.Log.LogFileOpenedEventArgs"/> instance containing the event data.</param>
        protected virtual void OnLogFileOpened(Object sender, LogFileOpenedEventArgs e)
        {
            EventHandler<LogFileOpenedEventArgs> tempLogFileOpened = LogFileOpened;
            if (tempLogFileOpened != null)
                tempLogFileOpened(sender, e);
        }

        /// <summary>
        /// Occurs when closing log file.
        /// </summary>
        public event EventHandler<ClosingLogFileEventArgs> ClosingLogFile;

        /// <summary>
        /// Called when closing log file.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="Td.Additional.Log.ClosingLogFileEventArgs"/> instance containing the event data.</param>
        protected virtual void OnClosingLogFile(Object sender, ClosingLogFileEventArgs e)
        {
            EventHandler<ClosingLogFileEventArgs> tempLogFileClosed = ClosingLogFile;
            if (tempLogFileClosed != null)
                tempLogFileClosed(sender, e);
        }

        #endregion
    }
}